using System.Drawing;
using System.Xml;
using Wilco.SyntaxHighlighting.Engine.Node;
using Wilco.SyntaxHighlighting.Engine.Node.Implementation.Css;
using Wilco.SyntaxHighlighting.Engine.Node.Occurrence;
using Wilco.SyntaxHighlighting.Engine.Scanner;
using Wilco.SyntaxHighlighting.Engine.Tokenizer;

namespace Wilco.SyntaxHighlighting.Engine.Scanner.Implementation.CSS
{
    /// <summary>
    /// Represents an CSS scanner.
    /// </summary>
    public class CssScanner : ScannerBase
    {
        private readonly CssAttributeNameNode cssSelectorNameNode;
        private readonly CssAttributeNameNode cssAttributeNameNode;
        private readonly CssAttributeValueNode cssAttributeValueNode;

        private int state;

        /// <summary>
        /// Initializes a new instance of an <see cref="CssScanner"/> class.
        /// </summary>
        public CssScanner() : base(null, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of an <see cref="CssScanner"/> class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="OccurrenceCollection"/> which will contain the scanner result.</param>
        public CssScanner(TokenizerBase tokenizer, OccurrenceCollection scannerResult) : base(tokenizer, scannerResult)
        {
            cssSelectorNameNode = new CssAttributeNameNode();
            cssSelectorNameNode.ForeColor = Color.Maroon;
            cssAttributeNameNode = new CssAttributeNameNode();
            cssAttributeNameNode.ForeColor = Color.Red;
            cssAttributeValueNode = new CssAttributeValueNode();
            cssAttributeValueNode.ForeColor = Color.Blue;
            SetID("CssScanner");
        }

        /// <summary>
        /// Initializes a new instance of a <see cref="IScanner"/> implementation class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="OccurrenceCollection"/> which will contain the scanner result.</param>
        /// <returns>A new instance of a <see cref="IScanner"/> implementation class.</returns>
        public override IScanner Create(TokenizerBase tokenizer, OccurrenceCollection scannerResult)
        {
            return new CssScanner(tokenizer, scannerResult);
        }

        /// <summary>
        /// Loads the state of the scanner.
        /// </summary>
        /// <param name="state">An <see cref="System.Object"/> that contains the state of the scanner.</param>
        public override void LoadState(object state)
        {
            XmlElement element = (XmlElement)state;
            LoadNode(element, cssSelectorNameNode, "SelectorNameNode");
            LoadNode(element, cssAttributeNameNode, "AttributeNameNode");
            LoadNode(element, cssAttributeValueNode, "AttributeValueNode");
        }

        /// <summary>
        /// Loads the information for a node.
        /// </summary>
        /// <param name="element">The <see cref="System.Xml.XmlElement"/> which contains the information about the node.</param>
        /// <param name="node">The <see cref="INode"/> implementation class for which the information will be set.</param>
        /// <param name="name">The name of the node.</param>
        private static void LoadNode(XmlNode element, INode node, string name)
        {
            FontConverter converter = new FontConverter();
            XmlElement nodeElement = (XmlElement)element.SelectSingleNode(string.Format("nodes/node[@name='{0}']", name));
            node.BackColor = ColorTranslator.FromHtml(element.SelectSingleNode("settings/setting[@name='BackColor']").InnerText);
            node.ForeColor = ColorTranslator.FromHtml(element.SelectSingleNode("settings/setting[@name='ForeColor']").InnerText);
            node.Font = (Font)converter.ConvertFromString(element.SelectSingleNode("settings/setting[@name='Font']").InnerText);
            node.NavigateUrl = element.SelectSingleNode("settings/setting[@name='NavigateUrl']").InnerText;
        }

        /// <summary>
        /// Saves the current state of the scanner.
        /// </summary>
        /// <param name="container">The container which will contain the state.</param>
        /// <returns>An <see cref="System.Object"/> that contains the state of the scanner.</returns>
        public override object SaveState(object container)
        {
            XmlDocument document = (XmlDocument)container;
            XmlElement element = (XmlElement)base.SaveState(container);

            // Save settings.
            XmlElement nodeRootElement = document.CreateElement("nodes");
            element.AppendChild(nodeRootElement);

            StoreNode(document, nodeRootElement, cssSelectorNameNode, "SelectorNameNode");
            StoreNode(document, nodeRootElement, cssAttributeNameNode, "AttributeNameNode");
            StoreNode(document, nodeRootElement, cssAttributeValueNode, "AttributeValueNode");

            return element;
        }

        /// <summary>
        /// Stores a node.
        /// </summary>
        /// <param name="document">The document which will contain the settings for this node.</param>
        /// <param name="nodeRootElement">The element which will hold the node's settings.</param>
        /// <param name="node">The node which should be represented.</param>
        /// <param name="name">The name of the node.</param>
        private static void StoreNode(XmlDocument document, XmlNode nodeRootElement, INode node, string name)
        {
            XmlElement nodeElement = document.CreateElement("node");
            nodeElement.SetAttribute("name", name);
            nodeRootElement.AppendChild(nodeElement);

            XmlElement settingRootElement = document.CreateElement("settings");
            nodeElement.AppendChild(settingRootElement);

            FontConverter converter = new FontConverter();
            settingRootElement.AppendChild(CreateSetting(document, "BackColor", ColorTranslator.ToHtml(node.BackColor)));
            settingRootElement.AppendChild(CreateSetting(document, "ForeColor", ColorTranslator.ToHtml(node.ForeColor)));
            settingRootElement.AppendChild(CreateSetting(document, "ForeColor", converter.ConvertToString(node.Font)));
            settingRootElement.AppendChild(CreateSetting(document, "NavigateUrl", node.NavigateUrl));
        }

        /// <summary>
        /// Creates a steting.
        /// </summary>
        /// <param name="document">The document which will contain the setting.</param>
        /// <param name="name">The name of the setting.</param>
        /// <param name="value">The value of the setting.</param>
        /// <returns>The <see cref="System.Xml.XmlElement"/> which represents the setting.</returns>
        private static XmlElement CreateSetting(XmlDocument document, string name, string value)
        {
            XmlElement element = document.CreateElement("setting");
            element.SetAttribute("name", name);
            element.InnerText = value;
            return element;
        }

        /// <summary>
        /// Scans a token.
        /// </summary>
        /// <remarks>
        /// An <see cref="IScanner"/> implementation will generally have a reference to a 
        /// <see cref="NodeCollection"/> which will be used to store results of a scan.
        /// </remarks>
        /// <param name="token">A token from the source code.</param>
        public override void Scan(string token)
        {
            if (!Enabled)
            {
                if (Child != null)
                {
                    Child.Scan(token);
                }
            }
            else
            {
                bool isMatch = false;

                char ch = token[0];

                if (!char.IsWhiteSpace(ch))
                {
                    // Match identifier(s).
                    if (state == 0)
                    {
                        if (ch == '{')
                        {
                            state = 1;
                            isMatch = true;
                        }
                        else
                        {
                            isMatch = MatchSelector();
                        }
                    }
                    else
                    {
                        if (ch == '}')
                        {
                            state = 0;
                            isMatch = true;
                        }
                        else
                        {
                            if (state == 1)
                            {
                                isMatch = MatchStyleName();
                            }
                            else if (state == 2)
                            {
                                isMatch = MatchStyleValue();
                            }
                        }
                    }
                }

                if (!isMatch)
                {
                    if (Child != null)
                    {
                        Child.Scan(token);
                    }
                }
            }
        }

        private bool MatchSelector()
        {
            int start = Tokenizer.Position;

            char ch = Tokenizer.Source[Tokenizer.Position];
            while (char.IsLetterOrDigit(ch) || ch == '-' || ch == '#')
            {
                if (Tokenizer.MoveNext())
                {
                    ch = Tokenizer.Source[Tokenizer.Position];
                }
                else
                {
                    Tokenizer.MoveTo(Tokenizer.Source.Length);
                    break;
                }
            }

            if (start < Tokenizer.Position)
            {
                ScannerResult.Add(new Occurrence(start, Tokenizer.Position - start, cssSelectorNameNode));

                return true;
            }

            return false;
        }

        private bool MatchStyleName()
        {
            int start = Tokenizer.Position;

            char ch = Tokenizer.Source[Tokenizer.Position];
            while (char.IsLetterOrDigit(ch) || ch == '-')
            {
                Tokenizer.MoveNext();
                ch = Tokenizer.Source[Tokenizer.Position];
            }

            if (start < Tokenizer.Position)
            {
                ScannerResult.Add(new Occurrence(start, Tokenizer.Position - start, cssAttributeNameNode));

                state = 2;
                return true;
            }

            return false;
        }

        private bool MatchStyleValue()
        {
            int end = Tokenizer.Source.IndexOf(";", Tokenizer.Position);
            if (end > -1)
            {
                ScannerResult.Add(new Occurrence(Tokenizer.Position, end - Tokenizer.Position, cssAttributeValueNode));
                Tokenizer.MoveTo(end);
                state = 1;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Resets the scanner.
        /// </summary>
        public override void Reset()
        {
            state = 0;
        }
    }
}